/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.connection.tcp;

import android.content.Context;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.AndroidReceiverException;
import com.hercules.android.receiver.ApplicationData;
import com.hercules.android.receiver.DebugHelper;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.app.DisplayApplicationActivity;
import com.hercules.android.receiver.connection.BaseServerConnection;
import com.hercules.android.receiver.connection.ConnectionUtil;
import com.hercules.android.receiver.connection.ServerConnectionException;
import com.hercules.android.receiver.connection.parser.CommandParsingException;
import com.hercules.android.receiver.connection.parser.EventEncodingException;
import com.hercules.android.receiver.model.commands.XRECommand;
import com.hercules.android.receiver.model.commands.XREConnectCommand;
import com.hercules.android.receiver.model.events.XREConnectEvent;
import com.hercules.android.receiver.model.events.XREEvent;
import com.hercules.android.receiver.painting.PaintersUtil;
import com.hercules.android.receiver.painting.XREObjectPainterException;
import com.hercules.android.receiver.painting.views.AndroidXREView;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>
 * This class is a concrete implementation of ServerConnection interface. It extends from BaseServerConnection.
 * </p>
 * <p>
 * Connections between receivers and server will be created using sockets. It uses java.util.logging package to perform
 * logging and MessageParser from "Android Receiver Parser" component to parse commands and encode events.
 * </p>
 * <p>
 * AndroidAssembly1 this class was heavily changed because of large number of bugs
 * <p>
 * Sample Usage:
 *
 * <pre>
 * // create TCPServerConnection instance
 * TCPServerConnection conn = new TCPServerConnection();
 *
 * // set fields using the setters
 * conn.setExceptionHandler(new DefaultExceptionHandler());
 * conn.setParser(new DefaultMessageParser());
 * conn.setReconnectAttempts(5);
 * conn.setReconnectAttemptsInterval(1000);
 * conn.setReconnectWaitInterval(1000);
 *
 * // create connect event
 * XREConnectEvent connectEvent = new XREConnectEvent();
 * // set the fields using setters
 * connectEvent.setApplicationName(&quot;app1&quot;);
 * connectEvent.setName(&quot;XREConnectEvent1&quot;);
 *
 * // create Android receiver
 * AndroidReceiver receiver = new AndroidReceiver();
 * receiver.setApplicationConnectData(new ApplicationConnectData());
 * receiver.getApplicationConnectData().setUrl(&quot;xre://localhost:5000/app1&quot;);
 * receiver.setApplicationData(new ApplicationData());
 * receiver.setCommandHandler(new AggregateXRECommandHandler());
 *
 * // connect some receiver:AndroiReceiver to server
 * conn.connect(connectEvent, receiver);
 * // sleep to wait for the response of the server
 * Thread.sleep(10000);
 *
 * // send some event to the server
 * String sessionGUID = receiver.getApplicationData().getSessionGUID();
 * XREEvent event = new XREResourceMeasuredEvent();
 * conn.send(sessionGUID, event);
 *
 * // close receiver connection to server
 * conn.close(sessionGUID);
 * </pre>
 *
 * </p>
 * <p>
 * <strong> Thread safety:</strong> Technically it is not thread safe since unpredictable results may occur if send and
 * close methods are called at the same time. Also it is mutable. However it is expected that first, setters will be
 * called to set the fields, next connect method will be called once, next send method will be called any number of
 * times and, at the end, the close method will be called. With these assumptions the class can be used in a thread safe
 * manner.
 * </p>
 *
 * @author DanLazar, morehappiness
 * @author 7realm
 * @version 1.0
 */
public class TCPServerConnection extends BaseServerConnection<TCPSessionInfo> {
    /** Represents pattern for xre server url. */
    private static final Pattern URL_PATTERN = Pattern.compile("xre\\://(.*?)(?:\\:(\\d*?))?/.+");
    /** Represents default connection port. */
    private static final int DEFAULT_PORT = 80;
    /** Represents xre header. */
    private static final byte[] XRE_HEADER = new byte[]{0x58, 0x52, 0x45, 0x0D, 0x0A};
    /** Represents command reading interval. */
    private static final int READ_SLEEP_INTERVAL = 20;
    /** Represents end of line marker. */
    private static final String EOL = "\r\n";

    /**
     * Creates a new instance.
     */
    public TCPServerConnection() {
        // empty
    }

    /**
     * This method connects the given receiver to a server. Should be called only once by each receiver, at startup.
     *
     * @param onConnectEvent the connection event
     * @param receiver the receiver for which connection is created
     * @throws IllegalArgumentException if either parameter is null
     */
    public void connect(XREConnectEvent onConnectEvent, AndroidReceiver receiver) {
        String methodName = "connect";
        LogUtil.enter(getLogger(), this, methodName, onConnectEvent, receiver);

        try {
            ConnectionUtil.checkNull(getLogger(), onConnectEvent, "onConnectEvent", null);
            ConnectionUtil.checkNull(getLogger(), receiver, "receiver", null);

            new TCPConnectionHandler(onConnectEvent, receiver).start();
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(getLogger(), this, methodName, e);
        }
        LogUtil.exit(getLogger(), this, methodName);
    }

    /**
     * This method will send the event to a server using the receiver connection identified by the given sessionGUID.
     * <p>
     * AndroidAssembly1 was changed entirely because of bugs
     *
     * @param receiver the android receiver to use
     * @param event the event that will be sent to the server
     * @throws IllegalArgumentException if any parameter is null or sessionGUID is empty string
     * @throws ServerConnectionException if any errors occurred while sending the event
     */
    public void send(XREEvent event, AndroidReceiver receiver) throws ServerConnectionException {
        String methodName = "send";
        LogUtil.enter(getLogger(), this, methodName, event, receiver);

        // check arguments
        ConnectionUtil.checkNull(getLogger(), event, "event", null);
        ConnectionUtil.checkNull(getLogger(), receiver, "receiver", null);
        ConnectionUtil.checkNull(getLogger(), receiver.getApplicationData(), "receiver.applicationData", null);

        // get session GUID
        String sessionGUID = receiver.getApplicationData().getSessionGUID();
        ConnectionUtil.checkNullOrEmpty(getLogger(), sessionGUID, "receiver.applicationData.sessionGUID");

        // get session
        TCPSessionInfo tcpSessionInfo = getSessions().get(sessionGUID);
        if (tcpSessionInfo == null) {
           /* throw LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
                "No session was found for session guid '" + sessionGUID + "'."));*/
        	LogUtil.warn(getLogger(), this, methodName, "No session was found for session guid '" + sessionGUID + "'.");
        	return;
        }

        try {
            try {
                // get the socket connection handler
                TCPConnectionHandler handler = tcpSessionInfo.getConnectionHandler();
                if (handler == null) {
                	throw LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
                            "No connection handler was found for session guid '" + sessionGUID + "'."));
                }

                // send event
            	handler.send(event);
            } catch (ServerConnectionException e) {
                // handle connection
                handle(e);
             //   retry(receiver);

                // resent command
             //   send(event, receiver);
                LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
                     "server connection error", e));
            }
        } catch (EventEncodingException e) {
            throw LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
                "Can not encode the XRE event '" + event + "'.", e));
        }/* catch (AndroidReceiverException e) {
            throw LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
                "Failed to retry connection for session guid '" + sessionGUID + "'.", e));
        }*/

        LogUtil.exit(getLogger(), this, methodName);
    }

    /**
     * Closes the connection for the receiver identified by the given sessionGUID.
     *
     * @param sessionGUID the session identifier
     * @throws IllegalArgumentException if sessionGUID is null/empty string
     */
    public void close(String sessionGUID) {
        String methodName = "close";
        LogUtil.enter(getLogger(), this, methodName, sessionGUID);

        ConnectionUtil.checkNullOrEmpty(getLogger(), sessionGUID, "sessionGUID");

        TCPSessionInfo tcpSessionInfo = getSessions().get(sessionGUID);
        
        if (tcpSessionInfo != null) {
        	ApplicationData applicationData = tcpSessionInfo.getConnectionHandler().getReceiver().getApplicationData();
        	Map<Integer, AndroidReceiver> nestedReceivers = applicationData.getNestedXREApplicationViewsIdToReceiver();
            if (nestedReceivers != null) {
            	for (AndroidReceiver nestedReceiver : nestedReceivers.values()) {
                    if (nestedReceiver != null) {
                    	try {
							nestedReceiver.getServerConnection().close(nestedReceiver.getApplicationData().getSessionGUID());
						} catch (ServerConnectionException e) {
							// nothing
						}
                    }
                }
            }
        	
            getSessions().remove(sessionGUID);
        	tcpSessionInfo.getConnectionHandler().shutdown();
        	if (Thread.currentThread() != tcpSessionInfo.getConnectionHandler()) {
        		tcpSessionInfo.getConnectionHandler().interrupt();
        		try {
					tcpSessionInfo.getConnectionHandler().join();
				} catch (InterruptedException e) {
					// nothing
				}
        	}
        }
        
        LogUtil.exit(getLogger(), this, methodName);
    }

    /** Represents set of receivers that are currently in retry state. */
    private Set<AndroidReceiver> retriedReceivers = Collections.synchronizedSet(new HashSet<AndroidReceiver>());

    /**
     * Retry connection for given receiver.
     *
     * @param receiver the android receiver
     * @throws AndroidReceiverException if there is problem with connection to receiver
     */
    private void retry(AndroidReceiver receiver) throws AndroidReceiverException {
        String methodName = "retry";

        if (retriedReceivers.contains(receiver)) {
            return;
        }

        retriedReceivers.add(receiver);

        try {
            // check if session guid is set
            boolean isReconnect =
                receiver.getApplicationData() != null && receiver.getApplicationData().getSessionGUID() != null;

            // shutdown session if it is active
            if (isReconnect) {
                String oldSessionGUID = receiver.getApplicationData().getSessionGUID();

                if (getSessions().containsKey(oldSessionGUID)) {
                    LogUtil.info(getLogger(), this, methodName, "Shutting down active receiver for session {0}",
                        oldSessionGUID);

                    // will also remove session
                    receiver.doShutdown(true);
                }
            }

            LogUtil.info(getLogger(), this, methodName, "Retry connection to {0}", receiver.getApplicationConnectData()
                .getUrl());

            // reconnect
            while (getCurrentReconnectAttempts() < getReconnectAttempts()) {
                increaseCurrentReconnectAttempts();

                // connect
                receiver.connect(isReconnect);

                // wait for connection to settle
                ConnectionUtil.sleep(getReconnectWaitInterval());

                // check if connected
                if (receiver.getApplicationData() != null
                    && getSessions().containsKey(receiver.getApplicationData().getSessionGUID())) {
                    LogUtil.info(getLogger(), this, methodName, "Successfully reconnected to {0}", receiver
                        .getApplicationConnectData().getUrl());

                    return;
                }

                // shutdown receiver
                receiver.doShutdown(true);

                // wait before next attempt
                ConnectionUtil.sleep(getReconnectAttemptsInterval());
            }

            LogUtil.info(getLogger(), this, methodName, "Failed to reconnect to {0}", receiver
                .getApplicationConnectData().getUrl());
        } finally {
            retriedReceivers.remove(receiver);
        }
    }

    /**
     * Handle the exception.
     *
     * @param e the exception to handle
     */
    private void handle(Exception e) {
        // handle exception
        if (getExceptionHandler() != null) {
            getExceptionHandler().handleException(e);
        }
    }

    /**
     * <p>
     * This class implements the Thread class and it is an inner class of TCPServerConnection class.
     * </p>
     * <p>
     * It is used to create a connection to a server for a receiver and to read the commands that are sent from the
     * server using a socket.
     * </p>
     * <p>
     * <strong> Thread safety:</strong> It is thread safe since it is immutable.
     * </p>
     *
     * @author DanLazar, morehappiness
     * @author 7realm
     * @version 1.0
     */
    public class TCPConnectionHandler extends Thread {
        /**
         * Represents the connection event.
         * <p>
         * Will be set in constructor and never changed. Used in run() method. Cannot be null.
         */
        private final XREConnectEvent onConnectEvent;

        /**
         * Represents the receiver for which the connection will be created.
         * <p>
         * Will be set in constructor and never changed. Used in run() method. Cannot be null.
         */
        private final AndroidReceiver receiver;

        /**
         * Represents the xre messages to be sent to server.
         */
        
        private final List<String> messages;
        
        private final AtomicBoolean shutdownStart = new AtomicBoolean(false);
        
        /**
         * Creates a new instance setting the fields with namesake parameters.
         *
         * @param onConnectEvent the connection event
         * @param receiver the receiver for which the connection is created.
         */
        public TCPConnectionHandler(XREConnectEvent onConnectEvent, AndroidReceiver receiver) {
            this.onConnectEvent = onConnectEvent;
            this.receiver = receiver;
            this.messages = Collections.synchronizedList(new LinkedList<String>());
        }

        /**
         * This method will create a connection using a socket to the server for the given receiver.
         * <p>
         * AndroidAssembly1 this method was entirely changed because of bugs
         * <p>
         * AndroidAssembly2 added debug part
         */
        @Override
        public void run() {
            final String methodName = "run";
            LogUtil.enter(getLogger(), this, methodName);

            Socket socket = null;
            DataInputStream socketInput = null;
            DataOutputStream socketOutput = null;

            try {
                // debug part, used to read commands from file
                if (DebugHelper.commandsInFile != null) {
                    readCommandsFromFile();
                    return;
                }

                // debug part, used for writing commands to file
                if (DebugHelper.commandsOutFile != null) {
                    // clear debug file before writing commands to it
                    receiver.getRootView().getContext().deleteFile(DebugHelper.commandsOutFile);
                }

                // get receiver url, create InetAddress object by the host
                String url = receiver.getApplicationConnectData().getUrl();
                socket = createSocket(url);

                try {
                    // get socket InputStream
                    socketInput = new DataInputStream(socket.getInputStream());
                    socketOutput = new DataOutputStream(socket.getOutputStream());

                    // send XRE header
                    socketOutput.write(XRE_HEADER);
                    socketOutput.flush();
                } catch (IOException e) {
                    throw new ServerConnectionException("Error occurred while setting session for url '" + url + "'.",
                        e);
                }

                if (shutdownStart.get()) {
                	return;
                }
                
                try {
                    // send onConnect event
                    writeToOutput(encodeEvent(onConnectEvent), socketOutput);
                } catch (EventEncodingException e) {
                    throw new ServerConnectionException("Error occurred while encoding onConnectEvent.", e);
                } catch (IOException e) {
                    throw new ServerConnectionException("Error occurred while sending onConnectEvent.", e);
                }

                // There is another problem with reconnection, but I guess it is because of bad architecture. Let's
                // imagine that we are receiving a lot of commands, for example 100 commands that will trigger on
                // animation. All this commands are sent by network almost immediately, and they are waiting at socket
                // buffer to read them. So last command index in server will be 100. But on client side, we read them
                // one by one and process immediately.

                // get first command
                String sessionGuid = null;
                try {
                    while (!shutdownStart.get()) {
                        if (socketInput.available() > 0) {
                            try {
                                // get xre command
                                final XRECommand command = readCommand(socketInput);
                                if (command instanceof XREConnectCommand) {
                                    // set session
                                    sessionGuid = ((XREConnectCommand) command).getSessionGUID();
                                    getSessions().put(sessionGuid, new TCPSessionInfo(socket, socketOutput, this));

                                    // pass command to the handler
									receiver.getCommandHandler().onCommandMessage(command, receiver);
                                } else {
                                    throw new ServerConnectionException(
                                        "Failed to connect to server. CONNECT command is expected.");
                                }

                                break;
                            } catch (CommandParsingException e) {
                                throw new ServerConnectionException("Failed to parse CONNECT command.");
                            }
                        }
                        ConnectionUtil.sleep(READ_SLEEP_INTERVAL);
                    }
                } catch (IOException e) {
                    throw new ServerConnectionException("Error occurred while receiving first command from server.", e);
                }

                LogUtil.info(getLogger(), this, "createSocket", "Successfully connected to {0}, session GUID {1}", url,
                    sessionGuid);

                try {
                    // process commands
                    while (!shutdownStart.get()) {
						
                        if (socketInput.available() > 0 || !messages.isEmpty()) {
                        	while (!messages.isEmpty() && !shutdownStart.get()) {
                            	String message = messages.remove(0);
                        		writeToOutput(message, socketOutput);
                            }
                        	
                        	if (socketInput.available() > 0) {
	                            try {
	                                // get xre command
	                                final XRECommand command = readCommand(socketInput);
	
	                                // pass command to the handler
	                                AndroidXREView.runOnUI(new Runnable() {
										public void run() {
											if (!shutdownStart.get()) {
												receiver.getCommandHandler().onCommandMessage(command, receiver);
											}
										}
                                    });
	                            	
	                            	// int length = socketInput.readInt();
	                            	// socketInput.
	                            } catch (CommandParsingException e) {
	                                // no need to reconnect on parsing error
	                            	LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
	                                        "parsing error.", e));
	                                handle(e);
	                            }
                        	}
                        	/*
                            if (!messages.isEmpty() && getSessions().containsKey(sessionGuid)) {
                            	String message = messages.remove(0);
                        		writeToOutput(message, socketOutput);
                            }*/
                        	
                        } else {
                            ConnectionUtil.sleep(READ_SLEEP_INTERVAL);
                        }
                    }
                } catch (IOException e) {
                    throw new ServerConnectionException("Error occurred while receiving commands from or sending messages server.", e);
                }
                
            } catch (ServerConnectionException e) {
                handle(e);
            /*    try {
                    retry(receiver);
                } catch (AndroidReceiverException e1) {
                    LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
                        "Failed to retry connection for receiver '" + receiver + "'.", e));
                }*/
                LogUtil.error(getLogger(), this, methodName, new ServerConnectionException(
                        "main connection error for receiver '" + receiver + "'.", e));
            } finally {
            	
            	LogUtil.warn(getLogger(), this, methodName, "connection closed: " + receiver.getApplicationConnectData().getApplicationName() + ".");
            	
                ConnectionUtil.close(socketOutput);
                ConnectionUtil.close(socketInput);
                // close the socket
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        LogUtil.error(getLogger(), this, methodName, e);
                    }
                }
            }

            LogUtil.exit(getLogger(), this, methodName);
        }

        private void shutdown() {
        	shutdownStart.set(true);
        }
        
        /**
         * Encode the event and put it to the buffer to be sent to server later.
         * @throws EventEncodingException if error occurred while encoding event
         */
        private void send(XREEvent event) throws EventEncodingException {
        	this.messages.add(encodeEvent(event));
        }
        
        public AndroidReceiver getReceiver() {
        	return this.receiver;
        }
        
        /**
         * Encodes the specified xre event to string.
         * <p>
         * AndroidAssembly3 new method, added to simplify API
         *
         * @param event the event to encode
         * @param output the server's output stream
         * @throws IOException if error occurred while sending message
         * @throws EventEncodingException if error occurred while encoding event
         */
        private String encodeEvent(XREEvent event) throws EventEncodingException {
            // set timestamp
            event.setTimestamp(System.currentTimeMillis());

            // encode event
            return (getParser().encodeEvent(event));
        }
        
        /**
         * Sends the specified message to the server.
         * <p>
         *
         * @param message the message to send
         * @param output the server's output stream
         * @throws IOException if error occurred while sending message
         */
        private void writeToOutput(String message, DataOutputStream output) throws IOException {
        	// log message
            LogUtil.info(getLogger(), this, "send", "Sending:  {0}", message);

            DebugHelper.recordEvent(receiver, message);
            
            // send event
            output.writeInt(message.length());
            output.write(message.getBytes());
            output.flush();
        }
        
        /**
         * Read commands from debug file.
         */
        private void readCommandsFromFile() {
            InputStream commandsFileInput = null;
            try {
                commandsFileInput = receiver.getRootView().getContext().openFileInput(DebugHelper.commandsInFile);

                StringBuilder commandsStr = new StringBuilder();
                byte[] buffer = new byte[1024];
                while (true) {
                    int count = commandsFileInput.read(buffer);
                    if (count == -1) {
                        break;
                    }

                    commandsStr.append(new String(buffer, 0, count));
                }
                String[] commands = commandsStr.toString().split(EOL);
                int commandIndex = 0;
                for (String commandStr : commands) {
                    List<XRECommand> xreCommands = getParser().parseCommands(commandStr);
                    XRECommand xreCommand = xreCommands.get(0);
                    xreCommand.setCommandIndex(commandIndex++);
                    receiver.getCommandHandler().onCommandMessage(xreCommand, receiver);
                }

            } catch (CommandParsingException e) {
                LogUtil.error(getLogger(), this, "run", e);
            } catch (IOException e) {
                LogUtil.error(getLogger(), e, "run", e);
            } finally {
                ConnectionUtil.close(commandsFileInput);
            }
        }

        /**
         * This method will read a command from the given {@link DataInputStream}.
         * <p>
         * AndroidAssembly1 this method is entirely changed
         *
         * @param inputStream the {@link DataInputStream} instance
         * @return the XRE connect command
         * @throws IOException if any I/O exception occurs
         * @throws CommandParsingException if commands string can not parse into XRE command list
         */
        private XRECommand readCommand(DataInputStream inputStream) throws IOException, CommandParsingException {
            byte[] commandBytes = readCommandFromStream(inputStream);

            // convert bytes to string
            String command = new String(commandBytes).trim();

            // log command
            LogUtil.info(getLogger(), this, "readCommand", "Received: {0}", command);

            // write command to file
            if (DebugHelper.commandsOutFile != null) {
                OutputStream commandsFileOutput =
                    receiver.getRootView().getContext()
                        .openFileOutput(DebugHelper.commandsOutFile, Context.MODE_APPEND);
                try {
                    commandsFileOutput.write(command.getBytes());
                    commandsFileOutput.write(EOL.getBytes());
                } finally {
                    commandsFileOutput.close();
                }
            }

            DebugHelper.recordCommand(receiver, command);
            
            // parse commands
            List<XRECommand> commands = getParser().parseCommands(command);

            // only one command can be here according to XRE specification
            if (commands.size() != 1) {
                throw new CommandParsingException("Exactly one command must be present in '" + command + "'.");
            }

            return commands.get(0);
        }

        /**
         * Reads command for stream.
         * <p>
         * AndroidAssembly2 new method, refactored from readCommand
         *
         * @param inputStream the input stream
         * @return the command bytes
         * @throws IOException if I/O error occurs
         * @throws CommandParsingException if end of stream reached
         */
        private byte[] readCommandFromStream(DataInputStream inputStream) throws IOException, CommandParsingException {
            // get first 4 bytes (these represent the command length)
            int length = inputStream.readInt();

            // read entire command
            int readed = 0;
            byte[] commandBytes = new byte[length];
            while (readed < length) {
                int count = inputStream.read(commandBytes, readed, length - readed);
                if (count == -1) {
                    throw new CommandParsingException("Unexpected end of command.");
                }

                readed += count;
            }
            return commandBytes;
        }

        /**
         * Creates a Socket object from the given url.
         * <p>
         * AndroidAssembly1 added url validation and sending of XRE default header
         *
         * @param url the url
         * @return the created socket
         * @throws NumberFormatException if a string can not parse into a valid integer
         * @throws ServerConnectionException if url is in incorrect format
         */
        private Socket createSocket(String url) throws ServerConnectionException {
            // According to XRE Specification, section '2.1 XRE Applications',
            // the XRE applications are identified by XRE URIs of the form:
            // xre://[host]:[port]/[application]
            Matcher matcher = URL_PATTERN.matcher(url);
            if (!matcher.find()) {
                throw new ServerConnectionException("Incorrect format of server url '" + url + "', expected pattern "
                    + URL_PATTERN.pattern() + "'.");
            }

            // get port and host
            String host = matcher.group(1);
            int port = matcher.group(2) == null ? DEFAULT_PORT : Integer.parseInt(matcher.group(2));

            try {
                // connect
                Socket socket = new Socket();
                socket.setReceiveBufferSize(150 * 1024);
                socket.connect(new InetSocketAddress(host, port), 10000);
                return socket;
            } catch (IOException e) {
                throw new ServerConnectionException("Error occurred while connecting to host '" + host + ":" + port
                    + "'.", e);
            }
        }
    }
}
